package cz.data.platform.web.utils;

import com.alibaba.fastjson.JSONObject;
import cz.data.common.utils.JsonCommon;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class SignUtils {
    public static final String X_CA_KEY = "x-ca-key";
    public static final String X_CA_NONCE = "x-ca-nonce";
    public static final String X_CA_TIMESTAMP = "x-ca-timestamp";
    public static final String X_CA_SIGNATURE = "x-ca-signature";

    static final byte[] CHUNK_SEPARATOR = {'\r', '\n'};

    public static byte[] encodeBase64(final byte[] binaryData) {
        return encodeBase64(binaryData, false, false, Integer.MAX_VALUE);
    }

    public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked,
                                      final boolean urlSafe, final int maxResultSize) {
        if (binaryData == null || binaryData.length == 0) {
            return binaryData;
        }

        // Create this so can use the super-class method
        // Also ensures that the same roundings are performed by the ctor and the code
        final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe);
        final long len = b64.getEncodedLength(binaryData);
        if (len > maxResultSize) {
            throw new IllegalArgumentException("Input array too big, the output array would be bigger (" +
                    len +
                    ") than the specified maximum size of " +
                    maxResultSize);
        }

        return b64.encode(binaryData);
    }

    private static Map<String, String> jsonObjectToMap(JSONObject jsonObject) {
        Map<String, String> collect = new HashMap<>();
        for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Collection) {
                Collection<?> collection = (Collection<?>) value;
                String collectionValue = collection.stream()
                        .filter(Objects::nonNull)
                        .map(Object::toString)
                        .collect(Collectors.joining(";"));
                collect.put(key, collectionValue);
            } else {
                collect.put(key, JsonCommon.toJson(value));
            }
        }
        return collect;
    }

    public static String sign(String secret, String method, String path,
                              JSONObject headers,
                              Map<String, String> querys,
                              JSONObject bodys) {
        return sign(secret, method, path,
                jsonObjectToMap(headers), querys,
                jsonObjectToMap(bodys), null);
    }

    public static String sign(String secret, String method, String path,
                              Map<String, String> headers, Map<String, String> querys,
                              Map<String, String> bodys, List<String> signHeaderPrefixList) {
        try {
            Mac hmacSha256 = Mac.getInstance("HmacSHA256");
            byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
            hmacSha256.init(new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256"));
            return new String(
                    encodeBase64(
                            hmacSha256.doFinal(
                                    buildStringToSign(method, path, headers, querys, bodys, signHeaderPrefixList).getBytes(StandardCharsets.UTF_8))
                    ), StandardCharsets.UTF_8);
        } catch (Exception var9) {
            throw new RuntimeException(var9);
        }
    }

    private static String buildStringToSign(String method, String path,
                                            Map<String, String> headers,
                                            Map<String, String> querys,
                                            Map<String, String> bodys,
                                            List<String> signHeaderPrefixList) {
        StringBuilder sb = new StringBuilder();
        sb.append(method.toUpperCase()).append("\n");
        if (null != headers) {
            if (null != headers.get("Accept")) {
                sb.append(headers.get("Accept"));
                sb.append("\n");
            }

            if (null != headers.get("Content-MD5")) {
                sb.append(headers.get("Content-MD5"));
                sb.append("\n");
            }

            if (null != headers.get("Content-Type")) {
                String contentType = headers.get("Content-Type");
                if (contentType.contains("boundary")) {
                    String[] strings = contentType.split(";");
                    for (String string : strings) {
                        if (!string.contains("boundary")) {
                            sb.append(string);
                            sb.append(";");
                        }
                    }

                    sb.deleteCharAt(sb.toString().length() - 1);
                } else {
                    sb.append(contentType);
                }
                sb.append("\n");
            }

            if (null != headers.get("Date")) {
                sb.append(headers.get("Date"));
                sb.append("\n");
            }
        }

        sb.append(buildHeaders(headers, signHeaderPrefixList));
        sb.append(buildResource(path, querys, bodys));
        return sb.toString();
    }

    private static String buildResource(String path, Map<String, String> querys, Map<String, String> bodys) {
        StringBuilder sb = new StringBuilder();
        if (!StringUtils.isBlank(path)) {
            sb.append(path);
        }

        Map<String, String> sortMap = new TreeMap<>();
        for (Map.Entry<String, String> entry : querys.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.isNotBlank(key)) {
                sortMap.put(key, entry.getValue());
            }
        }
        for (Map.Entry<String, String> entry : bodys.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.isNotBlank(key)) {
                sortMap.put(key, entry.getValue());
            }
        }

        StringBuilder sbParam = new StringBuilder();
        for (Map.Entry<String, String> entry : sortMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (StringUtils.isNotBlank(key)) {
                if (0 < sbParam.length()) {
                    sbParam.append("&");
                }
                sbParam.append(key);
                if (StringUtils.isNotBlank(value)) {
                    sbParam.append("=").append(value);
                }
            }
        }
        if (0 < sbParam.length()) {
            sb.append("?");
            sb.append(sbParam);
        }
        return sb.toString();
    }

    private static String buildHeaders(Map<String, String> headers, List<String> signHeaderPrefixList) {
        StringBuilder sb = new StringBuilder();
        if (null != signHeaderPrefixList) {
            signHeaderPrefixList.remove("x-ca-signature");
            signHeaderPrefixList.remove("Accept");
            signHeaderPrefixList.remove("Content-MD5");
            signHeaderPrefixList.remove("Content-Type");
            signHeaderPrefixList.remove("Date");
            Collections.sort(signHeaderPrefixList);
        }

        if (null != headers) {
            Map<String, String> sortMap = new TreeMap<>(headers);
            StringBuilder signHeadersStringBuilder = new StringBuilder();

            for (Map.Entry<String, String> header : sortMap.entrySet()) {
                if (isHeaderToSign(header.getKey(), signHeaderPrefixList)) {
                    sb.append(header.getKey());
                    sb.append(":");
                    if (!StringUtils.isBlank(header.getValue())) {
                        sb.append(header.getValue());
                    }

                    sb.append("\n");
                    if (0 < signHeadersStringBuilder.length()) {
                        signHeadersStringBuilder.append(",");
                    }

                    signHeadersStringBuilder.append(header.getKey());
                }
            }

            headers.put("x-ca-signature-headers", signHeadersStringBuilder.toString());
        }

        return sb.toString();
    }

    private static boolean isHeaderToSign(String headerName, List<String> signHeaderPrefixList) {
        if (StringUtils.isBlank(headerName)) {
            return false;
        } else if (headerName.startsWith("x-ca-")) {
            return true;
        } else {
            if (null != signHeaderPrefixList) {
                for (String signHeaderPrefix : signHeaderPrefixList) {
                    if (headerName.equalsIgnoreCase(signHeaderPrefix)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
}
